/**
 * The contents of this file are subject to the license and copyright detailed in the LICENSE and NOTICE files at the root of the source tree and available online at
 * http://www.askosi.org/license.html
 */
package be.destin.skos.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;

import org.apache.log4j.Logger;
import org.openrdf.repository.object.annotations.iri;

import be.destin.rdf.changes.StatusFilter;
import be.destin.skos.html.SkosHtmlManager;
import be.destin.skos.search.LabelMatchType;
import be.destin.skos.search.SearchResult;
import be.destin.skos.search.SearchUtil;
import be.destin.util.Util;

@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder={"notation","usage","hasTopConcept","concept"})
@iri(SchemeInterface.skosNamespace+"ConceptScheme")
/*
 * Fields from ISO25964 Thesaurus class:
 * dc:identifier = about
 * dc:contributor  (in ATHENA)
 * dc:coverage: to add?
 * dc:creator  (in ATHENA)
 * dc:date: to add?
 * dcterms:created, issued: to add?  (in ATHENA)
 * dcterms:modified: to add? Would be useful for harvesting...
 *      To generate automatically?
 * dc:description  (in ATHENA)
 * dc:format: to add?
 * dc:language: to add to specify supplementary languages apart from those of the title? To complete with "supraLanguage". Not really necessary...
 *      To generate automatically?  (in ATHENA)
 * dc:publisher: to add?
 * dc:relation: to add?
 * dc:rights: to add?
 * dc:source: to add for Copyright issues (in ATHENA)
 * dc:subject: to add?
 * dc:title  (in ATHENA)
 * dc:type: to add to specify if it is a thesaurus, an authority list, a taxonomy, a classification, etc. (value to normalize somehow?)
 * Allowed Qualifiers for labels: INN, BAN, USAN, Acronym, permutation, etc.
 * Allowed Qualifiers for each general type of relation (hierarchical, relation, equivalences)
 *   e.g. partitive, geographical, political, etc.
 * 
 */
public class ConceptScheme extends NoScheme implements SchemeUsageInterface {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	/** log4j category */
   private static Logger log = Logger.getLogger(ConceptScheme.class);

   protected LinkedList<NotationScheme> notations = new LinkedList<NotationScheme>();

   private LinkedList<Concept> concepts = new LinkedList<Concept>();
   private LinkedList<ConceptIdentifier> topConcepts = new LinkedList<ConceptIdentifier>();
 
   private Map<String/*handle*/,Concept> conceptMap = new ConcurrentHashMap<String,Concept>();
   private Map<String/*handle*/,HashSet<Concept>> labelsIndex = new ConcurrentHashMap<String,HashSet<Concept>>();

   protected Map<String/*complete code*/,String/*handle*/> aliasses = new ConcurrentHashMap<String,String>();

   protected SchemeUsageList schemeUsages = new SchemeUsageList();

   protected Set<String/*Application*/> applicationUsagesModified = new HashSet<String>();

   protected String prefixToRemove = "";
   
   protected int maxDepth = 0;
   
   private TreeSet<String> inError;
   private Map<String,Integer> matchedSchemes = new TreeMap<String,Integer>(); // how many times external schemes are mentionned in this scheme?
   
   private StatusFilter statusFilter = null;
   
   public ConceptScheme() {
	   super();
   }

   /**
    * 
    */
   public ConceptScheme(SkosManager managerInstance, String code) {
	   super(managerInstance, code);
   }

/*
 * STUPID duplication made mandatory by XmlBinding...
 */
   /*
   @XmlElement
   public LinkedList<String> getCreator() {
   	return creators;
   }

   public void setCreator(LinkedList<String> creators) {
   	this.creators = creators;
   }

   @XmlElement
   public LinkedList<String> getContributor() {
   	return contributors;
   }

   public void setContributor(LinkedList<String> contributors) {
   	this.contributors = contributors;
   }
   */

   /**
    * @return the supraLanguage
   @XmlElement
   public LinkedList<String> getSupraLanguage() {
   	if (this.supraLanguage == null) this.supraLanguage = new LinkedList<String>();
   	return this.supraLanguage;
   }
    */

/**
 * @return the displays
@XmlElement
public UrlList getDisplay() {
	return super.getDisplay();
}

@XmlElement
public UrlList getCreate() {
	return creates;
}


/**
 * @return the internal Note
@XmlElement
public String getInternalNote() {
	return super.getInternalNote();
}

/**
 * @return the helps
@XmlElement
public UrlList getHelp() {
	return helps;
}
*/

/**
 * @return the icons
@XmlElement
public UrlList getIcon() {
	return super.getIcon();
}

/**
 * @return the titles
@XmlElement
public TermList getTitle() {
	return super.getTitle();
}

/**
 * @return the descriptions
@XmlElement
public TermList getDescription() {
	return super.getDescription();
}

/**
 * @return the namespace
@XmlElement(name="namespace")
public String getNamespace() {
	return namespace;
}
*/

/*
 * END of DUPLICATION
 */

private void mapAlias() {
    for (Concept aConcept : this.getConceptMap().values()) {
    	for (LocalLinkedConcept anAlias : aConcept.getAlias()) {
    		this.putAlias(anAlias.getAbout(), aConcept.getAbout());
    	}
    }
}

private void mapIndexLabel() {
	// Ensures concepts aliasses are correcty indexed
	labelsIndex = new ConcurrentHashMap<String,HashSet<Concept>>(); // start from scratch!
    for (Concept aConcept : this.getConceptMap().values()) {
    	for (Term aTerm : aConcept.getLabel()) {
    		this.putIndexTerm(aTerm, aConcept);
    	}
    }
}

private String tellHierarchy(Concept[] uppers, int nbUppers, Concept addition, int top) {
	StringBuffer message = new StringBuffer();
	if (top >= 0) {
		message.append("Hierarchy Loop: ");
	} else {
		message.append("Hierarchy deeper than "+Concept.maxDepth+": ");
		top = 0;
	}
	for (int i=top; i < nbUppers; i++) {
		message.append(uppers[i].getScheme_About());
		message.append(" >> ");
	}
	return message.toString();
}

private void increaseDepth (Concept parent, Concept[] uppers, int nbUppers) {
	LinkedList<String> narrowers = parent.getAboutNarrower();
	if (narrowers == null || (narrowers.isEmpty()) ) return;
	if (parent.depth >= Concept.maxDepth) {
		int j = -1;
		for (int i=nbUppers-1; i >= 0; i--) {
			if (uppers[i].equals(parent)) {
				j = i;
				break;
			}
		}
		String message = parent.getScheme_About()+": "+tellHierarchy(uppers,nbUppers,parent,j);
		log.error(message);
		parent.addError(message);
//		for (int i=nbUppers-1; i >= 0; i--) {
//			message = uppers[i].getScheme_About()+": "+i+"th in a looping path? Depth="+uppers[i].depth;
//			uppers[i].addError(message);
//		}
		return; // Protection against loops in the hierarchy
	}
	if (this.maxDepth < parent.depth) {
		this.maxDepth = parent.depth;
	}
	for (int i=0; i < nbUppers; i++) {
		if (parent.getAbout().equals(uppers[i].getAbout())) {
			String message = parent.getScheme_About()+": "+tellHierarchy(uppers,nbUppers,parent,i);
			log.error(message);
			parent.addError(message);
			getErrors().add(message); // Bad enough to be explained at Scheme level
			return; // Protection against loops in the hierarchy
		}
	}
	uppers[nbUppers] = parent;
	for (String aNarrow: narrowers) {
		Concept narrow = this.getConceptMap().get(aNarrow);
		if (narrow != null) {
			if (narrow.depth <= parent.depth) {
				narrow.depth = parent.depth+1;
				increaseDepth(narrow,uppers, nbUppers+1);
			}
		}
	}
}

private void ensureNarrower() {
	// Scan all parents and add their children!
	boolean hierarchized = false;
	Concept[] uppers = new Concept[Concept.maxDepth];
	LinkedList<ConceptIdentifier> tops = new LinkedList<ConceptIdentifier>();
	for (Concept aConcept : this.getConcept()) {
		String about = this.getAbout();
		LinkedList<LocalLinkedConcept> parents = aConcept.getBroader();
		if (parents != null && (parents.size() > 0) ) {
			int nbBT = 0;
			for (LocalLinkedConcept aParent: parents) {
				String parentHandle = aParent.getAbout();
				if (parentHandle.equals(about)) {
					aConcept.addError(aConcept.getScheme_About()+": parent of itself?");
				}
				else {
					Concept parent = this.getConceptMap().get(parentHandle);
					if (parent != null) {
						hierarchized = true;
						nbBT++;
						if (parent.putNarrower(aConcept.getAbout(),aParent.getCurrentStatus(),aParent.getRelationQualifier())) {
							if (aConcept.depth <= parent.depth) {
								uppers[0] = parent;
								aConcept.depth = parent.depth+1;
								increaseDepth(aConcept,uppers,1);
							}
						}
					}
					else {
						aConcept.addError(aConcept.getScheme_About()+": broader "+parentHandle+" does not exist.");
					}
				}
			}
			if (nbBT > 0) {
				aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"broader",nbBT,null,false);
			}
		} else {
			tops.add(new ConceptIdentifier(aConcept.getAbout()));
		}
	}
	this.topConcepts = tops;

	for (Concept aConcept : this.getConcept()) {
		String about = this.getAbout();
		int nbNT = 0;
		for (LocalLinkedConcept aNarrowRel: aConcept.getNarrower()) {
			String aNarrow = aNarrowRel.getAbout();
			if (aNarrow != null && !aNarrow.isEmpty() && !aNarrow.equals(about)) {
				Concept narrow = this.getConceptMap().get(aNarrow);
				if (narrow != null) {
					hierarchized = true;
					nbNT++;
				}
			}
		}
		if (nbNT > 0) {
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"narrower",nbNT,null,false);
		}
	}

	if (hierarchized) {
		SchemeUsage su = new SchemeUsage(this,application, "broader");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"broader"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"broader"));
		this.putSchemeUsage(su); 
		su = new SchemeUsage(this,application, "narrower");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"narrower"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"narrower"));
		this.putSchemeUsage(su); 
	}
	this.hierarchized = hierarchized;
}

private void ensureReciprocalRelated() {
	// Scan all parents and add their children!
	boolean haveRelated = false;
	for (Concept aConcept : this.getConcept()) {
		String about = this.getAbout();
		LinkedList<LocalLinkedConcept> relatedList = aConcept.getRelated();
		if (relatedList != null) {
			int nbRT = 0;
			for (LocalLinkedConcept aRelated: relatedList) {
				String relatedHandle = aRelated.getAbout();
				if (relatedHandle.equals(about)) {
					aConcept.addError(aConcept.getScheme_About()+": related to himself?");
				}
				else {
					Concept relatedEntry = this.getConceptMap().get(relatedHandle);
					if (relatedEntry != null) {
						haveRelated = true;
						nbRT++;
						relatedEntry.putRelated(aConcept.getAbout(),aRelated.getCurrentStatus(),
								null/*AGROVOC has non symetrical relations: aRelated.getRelationQualifier()*/);
					}
					else {
						aConcept.addError(aConcept.getScheme_About()+": related "+relatedHandle+" does not exist.");
					}
				}
			}
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"related",nbRT,null,false);
		}
	}
	if (haveRelated) {
		SchemeUsage su = new SchemeUsage(this,application, "related");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"related"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"related"));
		this.putSchemeUsage(su); 
	}
}

public void ensureReciprocalMatch() {
	// Scan all parents and add their children!
	HashSet<String> broadMatchSchemes = new HashSet<String>();
	HashSet<String> narrowMatchSchemes = new HashSet<String>();
	HashSet<String> exactMatchSchemes = new HashSet<String>();
	HashSet<String> closeMatchSchemes = new HashSet<String>();
	HashSet<String> relatedMatchSchemes = new HashSet<String>();
	for (Concept aConcept : this.getConceptInMemory()) {
		HashSet<String> conceptMatchSchemes = new HashSet<String>();
		LinkedList<LinkedConcept> matches = aConcept.getBroadMatch();
		if (matches != null && !matches.isEmpty()) {
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"broadMatch",matches.size(),null,false);
			HashSet<String> matchedSchemes = SkosUtil.aboutSchemesRelations(matches);
			if (matchedSchemes.size() > 0) {
				conceptMatchSchemes.addAll(matchedSchemes);
				broadMatchSchemes.addAll(matchedSchemes);
			}
			for (LinkedConcept aBroader : matches) {
				String anAbout = aBroader.getAbout();
				Concept matchedConcept = this.managerInstance.getScheme_Concept(anAbout);
				if (matchedConcept != null) {
					if (matchedConcept.putNarrowMatch(aConcept.getScheme_About(),aBroader.getCurrentStatus(),aBroader.getRelationQualifier())) {
						matchedConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"narrowMatch",1,null,false);
					}
				}
			}
		}
		matches = aConcept.getNarrowMatch();
		if (matches != null && !matches.isEmpty()) {
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"narrowMatch",matches.size(),null,false);
			HashSet<String> matchedSchemes = SkosUtil.aboutSchemesRelations(matches);
			if (matchedSchemes.size() > 0) {
				conceptMatchSchemes.addAll(matchedSchemes);
				narrowMatchSchemes.addAll(matchedSchemes);
			}
			for (LinkedConcept aNarrower : matches) {
				String anAbout = aNarrower.getAbout();
				Concept matchedConcept = this.managerInstance.getScheme_Concept(anAbout);
				if (matchedConcept != null) {
					if (matchedConcept.putBroadMatch(aConcept.getScheme_About(),aNarrower.getCurrentStatus(),aNarrower.getRelationQualifier())) {
						matchedConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"broadMatch",1,null,false);
					}
				}
			}
		}
		matches = aConcept.getExactMatch();
		if (matches != null && !matches.isEmpty()) {
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"exactMatch",matches.size(),null,false);
			HashSet<String> matchedSchemes = SkosUtil.aboutSchemesRelations(matches);
			if (matchedSchemes.size() > 0) {
				conceptMatchSchemes.addAll(matchedSchemes);
				exactMatchSchemes.addAll(matchedSchemes);
			}
			for (LinkedConcept aMatch : matches) {
				String anAbout = aMatch.getAbout();
				Concept matchedConcept = this.managerInstance.getScheme_Concept(anAbout);
				if (matchedConcept != null) {
					if (matchedConcept.putExactMatch(aConcept.getScheme_About(),aMatch.getCurrentStatus(),aMatch.getRelationQualifier())) {
						matchedConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"exactMatch",1,null,false);
					}
				}
			}
		}
		matches = aConcept.getCloseMatch();
		if (matches != null && !matches.isEmpty()) {
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"closeMatch",matches.size(),null,false);
			HashSet<String> matchedSchemes = SkosUtil.aboutSchemesRelations(matches);
			if (matchedSchemes.size() > 0) {
				conceptMatchSchemes.addAll(matchedSchemes);
				closeMatchSchemes.addAll(matchedSchemes);
			}
			for (LinkedConcept aMatch : matches) {
				String anAbout = aMatch.getAbout();
				Concept matchedConcept = this.managerInstance.getScheme_Concept(anAbout);
				if (matchedConcept != null) {
					if (matchedConcept.putCloseMatch(aConcept.getScheme_About(),aMatch.getCurrentStatus(),aMatch.getRelationQualifier())) {
						matchedConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"closeMatch",1,null,false);
					}
				}
			}
		}
		matches = aConcept.getRelatedMatch();
		if (matches != null && !matches.isEmpty()) {
			aConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"relatedMatch",matches.size(),null,false);
			HashSet<String> matchedSchemes = SkosUtil.aboutSchemesRelations(matches);
			if (matchedSchemes.size() > 0) {
				conceptMatchSchemes.addAll(matchedSchemes);
				relatedMatchSchemes.addAll(matchedSchemes);
			}
			for (LinkedConcept aMatch : matches) {
				String anAbout = aMatch.getAbout();
				Concept matchedConcept = this.managerInstance.getScheme_Concept(anAbout);
				if (matchedConcept != null) {
					if (matchedConcept.putRelatedMatch(aConcept.getScheme_About(),aMatch.getCurrentStatus(),aMatch.getRelationQualifier())) {
						matchedConcept.putApplicationUsage(SkosHtmlManager.askosiDisplayApplication,"relatedMatch",1,null,false);
					}
				}
			}
		}
		for (String anAbout : conceptMatchSchemes) {
			Integer count = this.matchedSchemes.get(anAbout);
			if (count == null) count = Integer.valueOf(1);
			else count = Integer.valueOf(count.intValue()+1);
			this.matchedSchemes.put(anAbout,count);
		}
	}
	if (broadMatchSchemes.size() > 0) {
		SchemeUsage su = new SchemeUsage(this,application, "broadMatch");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"broadMatch"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"broadMatch"));
		this.putSchemeUsage(su); 
		for (String schemeAbout : broadMatchSchemes) {
			SchemeInterface aScheme = this.getManagerInstance().getScheme(schemeAbout);
			if (aScheme != null && aScheme instanceof ConceptScheme) {
				su = new SchemeUsage(((ConceptScheme)aScheme),application, "narrowMatch");
				su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"narrowMatch"));
				su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"narrowMatch"));
				((ConceptScheme)aScheme).putSchemeUsage(su); 
				((ConceptScheme)aScheme).setApplicationUsageModified(SkosHtmlManager.askosiDisplayApplication, true);
			}
		}
	}
	if (narrowMatchSchemes.size() > 0) {
		SchemeUsage su = new SchemeUsage(this,application, "narrowMatch");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"narrowMatch"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"narrowMatch"));
		this.putSchemeUsage(su); 
		for (String schemeAbout : narrowMatchSchemes) {
			SchemeInterface aScheme = this.getManagerInstance().getScheme(schemeAbout);
			if (aScheme != null && aScheme instanceof ConceptScheme) {
				su = new SchemeUsage(((ConceptScheme)aScheme),application, "broadMatch");
				su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"broadMatch"));
				su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"broadMatch"));
				((ConceptScheme)aScheme).putSchemeUsage(su); 
				((ConceptScheme)aScheme).setApplicationUsageModified(SkosHtmlManager.askosiDisplayApplication, true);
			}
		}
	}
	if (exactMatchSchemes.size() > 0) {
		SchemeUsage su = new SchemeUsage(this,application, "exactMatch");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"exactMatch"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"exactMatch"));
		this.putSchemeUsage(su); 
		for (String schemeAbout : exactMatchSchemes) {
			SchemeInterface aScheme = this.getManagerInstance().getScheme(schemeAbout);
			if (aScheme != null && aScheme instanceof ConceptScheme) {
				su = new SchemeUsage(((ConceptScheme)aScheme),application, "exactMatch");
				su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"exactMatch"));
				su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"exactMatch"));
				((ConceptScheme)aScheme).putSchemeUsage(su); 
				((ConceptScheme)aScheme).setApplicationUsageModified(SkosHtmlManager.askosiDisplayApplication, true);
			}
		}
	}
	if (closeMatchSchemes.size() > 0) {
		SchemeUsage su = new SchemeUsage(this,application, "closeMatch");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"closeMatch"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"closeMatch"));
		this.putSchemeUsage(su); 
		for (String schemeAbout : closeMatchSchemes) {
			SchemeInterface aScheme = this.getManagerInstance().getScheme(schemeAbout);
			if (aScheme != null && aScheme instanceof ConceptScheme) {
				su = new SchemeUsage(((ConceptScheme)aScheme),application, "closeMatch");
				su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"closeMatch"));
				su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"closeMatch"));
				((ConceptScheme)aScheme).putSchemeUsage(su); 
				((ConceptScheme)aScheme).setApplicationUsageModified(SkosHtmlManager.askosiDisplayApplication, true);
			}
		}
	}
	if (relatedMatchSchemes.size() > 0) {
		SchemeUsage su = new SchemeUsage(this,application, "relatedMatch");
		su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"relatedMatch"));
		su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"relatedMatch"));
		this.putSchemeUsage(su); 
		for (String schemeAbout : relatedMatchSchemes) {
			SchemeInterface aScheme = this.getManagerInstance().getScheme(schemeAbout);
			if (aScheme != null && aScheme instanceof ConceptScheme) {
				su = new SchemeUsage(((ConceptScheme)aScheme),application, "relatedMatch");
				su.setSearchURL(SkosHtmlManager.getAskosiSearchRelation(getManagerInstance(),"relatedMatch"));
				su.setAtomURL(SkosHtmlManager.getAskosiAtomRelation(getManagerInstance(),"relatedMatch"));
				((ConceptScheme)aScheme).putSchemeUsage(su); 
				((ConceptScheme)aScheme).setApplicationUsageModified(SkosHtmlManager.askosiDisplayApplication, true);
			}
		}
	}
}

public synchronized void preLoadFinished() {
	this.mapIndexLabel();
	this.mapAlias();
	this.ensureNarrower();
	this.ensureReciprocalRelated();
	this.ensureReciprocalMatch();
}

private void removeAlias (String anAlias, Concept aConcept) {
	if (aConcept.getAbout() == aliasses.get(anAlias)) {
		aliasses.remove(anAlias);
	}
}

/*
 * Remove the indexes pointing to a concept when this one is released from the cache (or from the scheme)
 */
protected void removeMappings(Concept aConcept) {
	LinkedList<String> aliasses = aConcept.getAboutAlias();
	if (aliasses != null)
		for (String anAlias : aliasses) {
			removeAlias(anAlias,aConcept);
		}
	LinkedList<Notation> notations = aConcept.getNotation();
	SkosManager skosManager = this.getManagerInstance();
	if (notations != null && skosManager != null) {
		for (Notation aNotation : notations) {
			SchemeInterface aScheme = skosManager.getScheme(aNotation.getScheme());
			if (aScheme != null && aScheme instanceof NotationScheme)
				((NotationScheme)aScheme).removeConcept(aNotation.getNotation(),aConcept);
		}
	}
	if (labelsIndex != null) {
		TermList labels = aConcept.getLabel();
		for (Term aTermLabel : labels) {
			String aLabelString = aTermLabel.getIndexLABEL();
			if (aLabelString!=null) {
				Set<Concept> candidates = this.labelsIndex.get(aLabelString);
				if (candidates != null) {
					if (candidates.remove(aConcept)) {
						// log.debug("REMOVE "+aLabelString+"="+candidates);
						if (candidates.size() == 0) this.labelsIndex.remove(aLabelString);
						// else labelsIndex.put(aLabelString, candidate): NO! candidates is already in labelsIndex!
					}
				}
			}
		}
	}

}

/**
 * @return the complete prefixed code of an entry (after applying alias)
 */
public String getAbout_Concept(String handle) {
	Concept aConcept = findAbout_Concept(handle);
	if (aConcept==null)return null;
	return getAbout_About(aConcept.getAbout());
}

public int applicationUsageTotal() {
	int total = 0;
	for (SchemeUsage aSchemeUsage: schemeUsages) {
		total += applicationUsageTotal(aSchemeUsage);
	}
	for (String anApplication: applicationUsagesModified.toArray(new String[0])) {
		setApplicationUsageModified(anApplication,false);
	}
	return total;
}

public int applicationUsageTotal(String anApplication) {
	int total = 0;
	Collection<SchemeUsage> usages = schemeUsages.getApplicationUsage(anApplication);
	if (usages != null) {
		for (SchemeUsage aSchemeUsage: schemeUsages) {
			total += applicationUsageTotal(aSchemeUsage);
		}
	}
	setApplicationUsageModified(anApplication,false);
	return total;
}

/*
private int applicationUsageTotal(String anApplication, String aRole) {
	SchemeUsage aRoleUsage = getApplicationUsage(anApplication, aRole);
	return applicationUsageTotal(anApplication, aRoleUsage);
}
*/

private synchronized long applicationUsageTotal(SchemeUsage aRoleUsage) {
	if (aRoleUsage == null) return 0;
	long total = 0;
	if (isApplicationUsageModified(aRoleUsage.getApplication())) {
		boolean isPropagated = aRoleUsage.isPropagated();
		for (Entry<String,ReferringApplication> aUsage: this.getConceptReferringApplication(aRoleUsage.getApplication()).entrySet()) {
			if (aUsage.getValue() != null) {
				int freq = aUsage.getValue().getCountCount(aRoleUsage.getRole());
				if (freq > 0) {
					Concept aConcept = this.getConcept(aUsage.getKey());
					if (aConcept != null) {
						aRoleUsage.putFreqMap(freq,aConcept);
						if (!isPropagated || (aConcept.getDepth()==0) ) {
							total += freq;
						}
					}
				}
			}
		}
		aRoleUsage.setTotal(total);
	}
	else {
		total = aRoleUsage.getTotal();
	}
	return total;
}

/**
 * @return the knownIndexes for an Application and a role
 */
public SchemeUsage getApplicationUsage(String application, String aRole) {
	return schemeUsages.find(application,aRole);
}

/**
 * @return the concepts
 */
@XmlElements( {
        @XmlElement(name="concept", type=Concept.class),
        @XmlElement(name="metadataProperty", type=MetadataProperty.class),
        @XmlElement(name="profile", type=Profile.class),
        @XmlElement(name="application", type=Application.class)
} )
public synchronized LinkedList<Concept> getConcept() {
	if (this.noConcepts) return null;
	if (this.concepts == null) this.concepts = new LinkedList<Concept>();
	return this.concepts;
}

@XmlTransient
public synchronized LinkedList<Concept> getAllConcepts() {
	if (this.concepts == null) this.concepts = new LinkedList<Concept>();
	return concepts;
}

@XmlElement
public synchronized LinkedList<ConceptIdentifier> getHasTopConcept() {
	return this.topConcepts;
}

public LinkedHashMap<String,ReferringApplication> getConceptReferringApplication(String application) {
	LinkedHashMap<String,ReferringApplication> result = new LinkedHashMap<String,ReferringApplication>();
	if (this.concepts != null) {
		for (Concept aConcept: this.getConceptInMemory() ) {
			ReferringApplication aList = aConcept.getApplicationUsage(application);
			if (aList != null) result.put(aConcept.getAbout(),aList);
		}
	}
	return result;
}

/**
 * @return the concepts for an handle (redirected or not). Null if none
 */
public synchronized LinkedList<Concept> getConcept(Collection<String> handles) {
	if (handles == null) return null;
	Concept aConcept;
	LinkedList<Concept> result = new LinkedList<Concept>();
	for (String anHandle : handles) {
		aConcept = this.getConcept(anHandle);
		if (aConcept != null) result.add(aConcept);
	}
	return result;
}

/**
 * @return the concepts for an handle (redirected or not). Null if none
 */
public synchronized Concept getConcept(String handle) {
	if (handle == null) return null;
	handle = SkosUtil.normalizeAbout(handle);
	if (handle.length()==0) return null;
	if (this.getConceptMap().containsKey(handle)) return this.conceptMap.get(handle);
	if (this.aliasses.containsKey(handle)) {
		String aKey = this.aliasses.get(handle);
		return this.conceptMap.get(aKey);
	}
	return null;
}
/**
 * @return the concepts
 */
@XmlTransient
public synchronized Map<String,Concept> getConceptMap() {
	if (this.concepts == null) return null;
	if (this.conceptMap == null || this.concepts.size() != this.conceptMap.size()) {
		this.conceptMap = new ConcurrentHashMap<String,Concept>();
		for (Concept aConcept : concepts) {
			aConcept.inScheme = this; // As this is not always set...
			this.conceptMap.put(aConcept.getAbout(),aConcept);
		}
	}
	return this.conceptMap;
}
/**
 * @return the notations
 */
@XmlElement
public LinkedList<NotationScheme> getNotation() {
	if (this.notations == null) this.notations = new LinkedList<NotationScheme>();
	return notations;
}

public NotationScheme getNotation(String notationAbout) {
	NotationScheme result = null;
	for (NotationScheme notatScheme : getNotation()) {
		if (notationAbout.equalsIgnoreCase(notatScheme.getAbout())) {
			result = notatScheme;
			break;
		}
	}
	return result;
}

/**
 * @return the aliasses
 */
@XmlTransient
public Map<String, String> getAlias() {
	if (this.aliasses == null) this.aliasses = new ConcurrentHashMap<String,String>();
	return aliasses;
}

/**
 * @return the knownIndexes
 */
@XmlElement
public SchemeUsageList getUsage() {
	return this.schemeUsages;
}

public SchemeUsage getUsage(String application, String role) {
	SchemeUsage usage = (this.getUsage().find(application, role));
	if (usage == null) {
		usage = new SchemeUsage( this, application, role);
		this.putSchemeUsage(usage);
	}
	return usage;
}

/**
 * @return the statusFilter
 */
@XmlTransient
public synchronized StatusFilter getStatusFilter() {
	if (statusFilter == null) {
		// TODO: Decide what is the best for DynamicScheme
		statusFilter = new StatusFilter(getConceptInMemory());
	}
	return statusFilter;
}

/**
 * @param the knownIndexes for an Application and a role
 */
public synchronized boolean putSchemeUsage(SchemeUsage aRoleUsage) {
	if (aRoleUsage == null) return false;
	String application = aRoleUsage.getApplication();
	if (application == null) return false;
	application = application.trim();
	if (application.length()==0) return false;
	if (aRoleUsage.getRole() == null )return false;

	NoScheme currScheme = aRoleUsage.getInScheme();
	if (currScheme != this) {
		if (currScheme == null) {
			aRoleUsage.setInScheme(this);
		} else {
			String inAbout = aRoleUsage.getInScheme().getAbout();
			if (this.getAbout().equalsIgnoreCase(inAbout)) {
				aRoleUsage.setInScheme(this);
			} else {
				NotationScheme ns = this.getNotation(inAbout);
				if (ns != null) aRoleUsage.setInScheme(ns);
			}
		}
	}
	if (schemeUsages.put(aRoleUsage)) {
		setApplicationUsageModified(application,true);
		return true;
	}
	return false;
}

/**
 * @param entry to put amongst concepts
 */
public synchronized Concept putConcept(Concept entry) {
	String aCode = entry.getAbout();
	if (aCode == null) {
		String message = "ConceptScheme cannot receive concepts without code";
		log.error(message);
		this.getErrors().add(message);
	}
	Concept oldValue = this.conceptMap.get(aCode);
	if (oldValue == null) {
		entry.setInScheme(this);
		this.conceptMap.put(aCode, entry);
		this.concepts.add(entry); // Absolutely new
		entry.cleanUp();
		entry.setNotation(entry.getNotation());
	}
	else if (oldValue != entry) { // MERGE with a different entry
		//this.removeConcept(oldValue);
		//this.concepts.add(entry);
		oldValue.mergeImporting(entry);
		return oldValue;
	} // else entry already there!
	return entry;
}

/**
 * @param handle of the entry to put amongst concepts
 * @param entry to put amongst concepts
 */
public synchronized Concept putConcept(String handle, Concept entry) {
	entry.setAbout(handle); /* ensure coherence */
	return this.putConcept(entry);
}

/**
 * @param handle of the entry to put amongst concepts
 * @param entry to put amongst concepts
 */
public synchronized void removeConcept(Concept entry) {
	removeMappings(entry);
	this.concepts.remove(entry);
}

/**
 * @param notations the notations to set
 */
public void putNotationScheme(NotationScheme notations) {
	if (notations == null) return;
	if ( ! this.notations.contains(notations)) this.notations.add(notations);
}

/**
 * @param origin: handle of the entry to redirect
 * @param handle of the entry to which the origin is redirected
 */
public void putAlias(String origin, String handle) {
	if (origin == null) return;
	origin = SkosUtil.normalizeAbout(origin);
	if (origin.length()==0) return;
	if (handle == null) return;
	handle = SkosUtil.normalizeAbout(handle);
	if (handle.length()==0) return;
	putAliasValidated(origin, handle);
}

protected void putAliasValidated(String origin, String handle) {
	this.aliasses.put(origin, handle);
}

/**
 * @param conceptMap the concepts to set
 */
public synchronized void setConcept(List<Concept> someEntries) {
/*	if (someEntries instanceof LinkedList) { Dangerous optimization especially for XML loading
		this.concepts = (LinkedList<Concept>) someEntries;
		for (Concept aConcept : this.concepts) {
			this.conceptMap.put(aConcept.getAbout(),aConcept);
		}
	}
	else */ {
		if (concepts != null) {
			LinkedList<Concept> toBeRemoved = new LinkedList<Concept>();
			for ( Concept aConcept : concepts ) {
				if ( ! someEntries.contains(aConcept)) {
					toBeRemoved.add(aConcept);
				}
			}
			if (toBeRemoved.size() > 0) {
				log.info("Removal of "+toBeRemoved.size()+" concepts.");
				for ( Concept aConcept : toBeRemoved ) {
					this.removeConcept(aConcept);
				}
			}
		} else {
			concepts = new LinkedList<Concept>();
		}
		log.info("Addition of "+someEntries.size()+" concepts.");
		for (Concept aConcept: someEntries) {
			putConcept(aConcept);  // Necessary to put the concept InScheme !
		}
	}
}

protected synchronized void reloadConcept(List<Concept> someEntries) {
	if (concepts != null) {
		labelsIndex = null;
		HashSet<Concept> toBeKept = new HashSet<Concept>();
		HashSet<Concept> toAdd = new HashSet<Concept>();
		for (Concept aConcept : someEntries) {
			String anAbout = aConcept.getAbout();
			Concept existing = this.getConcept(anAbout);
			if (existing != null) { // Replace (not merge) new concept
				existing.reloadConcept(aConcept);
				toBeKept.add(existing);
			} else {
				existing = putConcept(aConcept);  // Necessary to put the concept InScheme !
				toAdd.add(existing);
				existing.setUsage(aConcept.getUsage());
			}
		}
		HashSet<Concept> toBeRemoved = new HashSet<Concept>();
		for ( Concept aConcept : concepts ) {
			if ( !toBeKept.contains(aConcept) && !toAdd.contains(aConcept) ) {
				toBeRemoved.add(aConcept);
			}
		}
		for ( Concept aConcept : toBeRemoved ) {
			this.removeConcept(aConcept);
		}
		String message = "Scheme Reload at "+(new Date()).toString()+": "
				+someEntries.size()+" concepts, "
				+toBeKept.size()+" reloaded, "
				+toAdd.size()+" added, "
				+toBeRemoved.size()+" removed.";
		log.info(message);
		this.getErrors().add(message);

	} else {
		concepts = new LinkedList<Concept>();
		for (Concept aConcept : someEntries) {
			putConcept(aConcept);  // Necessary to put the concept InScheme !
			aConcept.setUsage(aConcept.getUsage()); // To index the usage
		}
	}
}

/**
 * @param conceptMap the concepts to set
 */
public synchronized void setConceptMap(Map<String, Concept> someEntries) {
	this.conceptMap = someEntries;
}

/**
 * @param notations the notations to set
 */
public void setNotation(LinkedList<NotationScheme> notations) {
	this.notations = notations;
}

public void setNotation(Collection<NotationScheme> notations) {
	this.notations = new LinkedList<NotationScheme>(notations);
}

/**
 * @param aliasses the aliasses to set
 */
public void setAlias(Map<String, String> aliasses) {
	this.aliasses = aliasses;
}

/**
 * @param knownIndexes the knownIndexes to set
 */
public void setUsage(SchemeUsageList knownIndexes) {
	for (SchemeUsage aUsage: knownIndexes) {
		this.putSchemeUsage(aUsage);
	}
}

/*
 * How many concepts in this scheme?
 */
public int getSize() {
	return concepts.size();
}

/*
 * getNotationConcept is an helper for NotationScheme when the primary Scheme knows better how to find a given concept.
 * Used primarily by DynamicScheme (all others are happy with the implementation below)
 */
public Concept getNotationConcept (String indexCode, String notationCode) {
	for (NotationScheme theNotation : this.notations) {
		if (theNotation.getAbout().equals(indexCode)) {
			Concept aConcept = theNotation.getConcept(notationCode);
			return aConcept;
		}
	}
	return null;
}

public Set<Concept> getIndexLabel(String aLabel) {
	if (labelsIndex == null) return null; // Reload in progress!
	String LABEL = Util.noAccent(aLabel.toUpperCase().trim()); 
	log.debug("get "+LABEL+"="+this.labelsIndex.get(LABEL));
	return this.labelsIndex.get(LABEL);
}

synchronized void putIndexTerm(Term aTerm, Concept aConcept) {
	if (labelsIndex != null) { // Reload in progress!
		if (aTerm == null) return;
		String LABEL = aTerm.getIndexLABEL();
		if (LABEL == null) return;
		HashSet<Concept> candidates = this.labelsIndex.get(LABEL);
		if (candidates == null) {
			candidates = new HashSet<Concept>();
			this.labelsIndex.put(LABEL, candidates);
		}
		candidates.add(aConcept);
		// log.debug("put "+LABEL+"="+this.labelsIndex.get(LABEL));
		// labelsIndex.put(aLabelString, candidate): NO! candidates is already in labelsIndex!
	}
}

public ArrayList<SearchResult> labelMatch(String query, String userLanguage, boolean strictLanguage, LabelMatchType matchType) {
	return SearchUtil.labelMatch(	this.getManagerInstance(),
							query, userLanguage, strictLanguage, 
							matchType, matchType==LabelMatchType.EXACT ? this.getIndexLabel(query) : this.getConceptMap().values());
}

public ArrayList<SearchResult> labelMatch(String query, String userLanguage, boolean strictLanguage, LabelMatchType matchType, Collection<Concept> filter) {
	Collection<Concept> filterSet = new HashSet<Concept>();
	if (filter != null) {
		filterSet.addAll(filter);
		if (matchType == LabelMatchType.EXACT) {
			filterSet.retainAll(this.getIndexLabel(query));
		} else {
			filterSet.retainAll(this.getConceptMap().values());
		}
		if (filterSet.isEmpty()) {
			return new ArrayList<SearchResult>(); // Empty result
		}
	} else {
		if (matchType==LabelMatchType.EXACT) {
			filterSet = this.getIndexLabel(query);
		} else {
			filterSet = this.getConceptMap().values();
		}
	}
	return SearchUtil.labelMatch( this.getManagerInstance(), query, userLanguage, strictLanguage, matchType, filterSet );
}

/**
 * @return the prefixToRemove
 */
@XmlTransient
public String getPrefixToRemove() {
	return prefixToRemove;
}

public String getCleanAbout(String currCode) {
	String prefixToRemove = this.getPrefixToRemove();
	if (prefixToRemove != null
			&& prefixToRemove.length() > 0
			&& currCode.startsWith(prefixToRemove))
		currCode = currCode.substring(prefixToRemove.length());

	prefixToRemove = getNamespace();
	if (prefixToRemove != null
			&& prefixToRemove.length() > 0
			&& currCode.startsWith(prefixToRemove))
		currCode = currCode.substring(prefixToRemove.length());

	String root = getAbout()+SchemeInterface.separator;
	if (currCode.startsWith(root))
		currCode = currCode.substring(root.length());
	return currCode;
}

/**
 * @param prefixToRemove the prefixToRemove to set
 */
public void setPrefixToRemove(String prefixToRemove) {
	this.prefixToRemove = prefixToRemove;
}

/*
 * Does this scheme has broader/narrower relations?
 */
@XmlTransient
public boolean isHierarchized() {
	return this.hierarchized;
}

public boolean isApplicationUsageModified(String application) {
	return this.applicationUsagesModified.contains(application);
}

public void setApplicationUsageModified(String application, boolean modified) {
	if (application == null) return;
	if (modified) this.applicationUsagesModified.add(application);
	else this.applicationUsagesModified.remove(application);
}

/* What is the max depth of the hierarchy in this scheme?
 * 
 */
@XmlTransient
public int getMaxDepth() {
	return this.maxDepth;
}

/**
 * This function returns the presentation of a link to create a new concept
 * @return the HTML presentation of an authority entry creation
 */
public String toHtmlCreation(SkosHtmlManager skosHtmlManager, String createIcon, boolean schemeIcon) {
	String aName = getTitle(skosHtmlManager.getLanguage());
	if (aName == null || aName.length() <= 0) aName = this.getAbout();
	String create = getCreateAdapted(skosHtmlManager);
	if (create == null || create.isEmpty()) return "";
	String icon = getIconAdapted(skosHtmlManager);
	return "<a href=\""
				+create
				+"\" class=\"skosCreate\" target=\"creation_"+this.getAbout()+"\">"
		 + createIcon
		 + ( schemeIcon && icon != null ?
				 skosHtmlManager.toHtmlImage(icon, aName, false)
				 : "")
		 + "</a>";
}

/**
 * @return the inError
 */
@XmlTransient
public TreeSet<String> getInError() {
	if (inError == null) inError = new TreeSet<String>();
	return inError;
}

/**
 * @param inError the inError to set
 */
public void setInError(TreeSet<String> inError) {
	this.inError = inError;
}

/**
 * @return the matchedSchemes
 */
@XmlTransient
public Set<String> getMatchedSchemes() {
	return matchedSchemes.keySet();
}

@XmlTransient
public Map<String,Integer> getMatchedSchemesMap() {
	return matchedSchemes;
}

public int checkDuplicatedLabel(TreeMap<String,String> allLabels,boolean languageSensitive) {
	int total = 0;
	for (Concept aConcept: this.getConcept()) {
		total += aConcept.checkDuplicatedLabel(allLabels,languageSensitive);
	}
	return total;
}


public boolean equals(Object o) {
	return super.equals(o);
}

public static LinkedList<Concept> mergeDown(List<Concept> conceptList) {
	HashMap<String,Concept> result = new HashMap<String,Concept>();
	for (Concept aConcept : conceptList) {
		String about = aConcept.getAbout();
		if (result.containsKey(about)) {
			Concept prevConcept = result.get(about);
			prevConcept.mergeImporting(aConcept);
		} else {
			result.put(about, aConcept);
		}
	}
	return new LinkedList<Concept>(result.values());
}
}
